Skip to content

BridgeJS: Support generic functions on imported JS APIs#18

Draft
krodak wants to merge 3 commits into
mainfrom
kr/stack-abi-generics-import
Draft

BridgeJS: Support generic functions on imported JS APIs#18
krodak wants to merge 3 commits into
mainfrom
kr/stack-abi-generics-import

Conversation

@krodak

@krodak krodak commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator

Import-side half of BridgeJS generic function support (split out of the upstream generics PR per review), reworked around the review feedback on the shared ABI. Covers the import scenarios from the upstream generics-support request.

What this enables

@JSFunction func parse<T: BridgedSwiftGenericBridgeable>(_ json: String) throws(JSException) -> T

let user: User = try parse(jsonString)   // T inferred from the call site

One declaration serves every bridged type — primitives, String, @JS structs/enums/final classes, and T? / [T] / [String: T] compositions — instead of a wrapper per deserialized type (OpenAPI clients, JSON.parse, storage reads, message payloads). Also supported on @JSClass methods and statics. Return-only generics work (T inferred from the return type). Generic imports are Embedded-compatible and exercised by Examples/Embedded.

Design (incorporates the upstream review feedback)

  1. Handle-based type identity (replaces string interning): each conforming type owns a BridgeJSTypeHandle; typeID is the handle's pointer. No name-based identity anywhere on the wire — cross-module name conflicts are structurally impossible. Under Embedded the handle is a pure identity token (the type existential property is compiled out; imports are statically specialized and never need it).
  2. Registration: each module exposes bjs_<Module>_register_type_handles, pairing handle IDs with JS codecs index-by-index in a canonical order shared by Swift codegen and the link layer. Trigger is hybrid: eager via an optional afterInitialize() instantiator hook called after wasi.initialize() (registration executes Swift, so it cannot run during glue setup), with a lazy first-generic-call fallback for worker threads and hand-rolled hosts.
  3. Unconditional conformance emission: a module cannot know its dependents, so BridgedSwiftGenericBridgeable conformances are emitted for all @JS types regardless of whether the defining module declares generics — a type from module A works as a generic argument in module B. The JS-side generic runtime stays link-gated (the linker sees all modules), so fully non-generic builds pay nothing in the bundle.
  4. Combinator codecs: container stack ABIs are described once — __bjs_arrayCodec(elem), __bjs_optionalCodec(elem), __bjs_dictCodec(value) — and generic paths compose them. (Routing the non-generic inline emission through the same combinators is proposed as a follow-up formalization step.)

The constraint protocol is BridgedSwiftGenericBridgeable (public spelling, since users write it; conformances are runtime/codegen-owned — the sole requirement is SPI-gated). Generic parameters on exported @JS functions are rejected with a clear diagnostic in this PR; the export side (type reification via handle recovery, BridgeType<T> tokens, @JS protocol constraints) lands separately on top of this ABI.

Testing

  • Tool tests: codegen + link snapshots for the full matrix (free functions, class methods/statics, multi-parameter, return-only, arrays/optionals/dictionaries), import diagnostics, export-rejection diagnostics, registration/gating link tests, same-named-types-across-modules link test.
  • Runtime E2E: ImportGenericAPITests (Swift Testing) round-trips scalars, String, structs, enums, classes, compositions through real JS implementations (including JSON.parse).
  • Embedded: Examples/Embedded builds and runs a generic import round-trip.
  • Full suite: BridgeJS plugin tests, zero generated-artifact drift, check:bridgejs-dts, WASM E2E (make unittest), formatter.

krodak added 3 commits July 15, 2026 08:36
The non-generic bridging paths cloned each container's stack ABI (array
loops with the typed-array fast path, dictionary key/value ordering, the
optional presence-flag protocol) inline into every thunk, while the
generic paths already described those shapes once via the codec
combinators. Route the non-generic array/dictionary/optional emission
through the same combinators so each container's stack ABI is described
exactly once and instantiated with an element codec:

- __bjs_primitiveCodecs becomes a token-keyed table (Bool, Int, ...,
  String, JSValue) referenced by both combinator instantiations and the
  generic type-handle registration hooks (same canonical order).
- @js struct elements compose directly with structHelpers.<T> (already
  codec-shaped); associated-value enums adapt their helper through the
  new __bjs_enumCodec combinator; other element shapes (case enums,
  JSObject, heap objects, ...) get a local codec literal built from the
  single element stack fragment description.
- __bjs_optionalCodec gains an isUndefinedOr parameter so the
  JSUndefinedOr flavor instantiates the same combinator instead of a
  bespoke inline form; stack-convention optional parameters and returns
  now route through it too. Nested compositions recurse
  (e.g. __bjs_arrayCodec(__bjs_optionalCodec(__bjs_primitiveCodecs.Int))).
- Emission gating: the combinators and the primitive codec table are
  emitted through the intrinsic registry whenever any linked module uses
  container bridging (generic or not); builds bridging no containers pay
  nothing. The generic runtime (codecByTypeId, registration hooks,
  afterInitialize) stays gated on generics, independently.
- Paths that cannot use the combinators keep inline code with comments:
  side-channel/sentinel optional returns, and optional parameters whose
  presence flag arrives as a wasm parameter rather than on the i32 stack.

Deletes the inline array/dictionary loop fragments, stackOptionalLower,
and optionalLiftReturnStruct from JSGlueGen; snapshots re-recorded.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant